package com.ruoyi.common.utils.reflectasm;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;

import static org.objectweb.asm.Opcodes.*;

public abstract class MethodAccess {
  private String[] methodNames;
  private Class<?>[][] parameterTypes;
  private Class<?>[] returnTypes;

  abstract public Object invoke(Object object, int methodIndex, Object... args);

  /** 调用具有指定名称和指定参数类型的方法 */
  public Object invoke(Object object, String methodName, Class<?>[] paramTypes, Object... args) {
    return invoke(object, getIndex(methodName, paramTypes), args);
  }

  /** 调用具有指定名称和指定数量参数的第一个方法 */
  public Object invoke(Object object, String methodName, Object... args) {
    return invoke(object, getIndex(methodName, args == null ? 0 : args.length), args);
  }

  /** 返回具有指定名称的第一个方法的索引 */
  public int getIndex(String methodName) {
    for (int i = 0, n = methodNames.length; i < n; i++)
      if (methodNames[i].equals(methodName)) return i;
    throw new IllegalArgumentException("Unable to find non-private method: " + methodName);
  }

  /** 返回具有指定名称和参数类型的第一个方法的索引 */
  public int getIndex(String methodName, Class... paramTypes) {
    for (int i = 0, n = methodNames.length; i < n; i++)
      if (methodNames[i].equals(methodName) && Arrays.equals(paramTypes, parameterTypes[i]))
        return i;
    throw new IllegalArgumentException("Unable to find non-private method: " + methodName + " " + Arrays.toString(paramTypes));
  }

  /** 返回具有指定名称和指定数量参数的第一个方法的索引 */
  public int getIndex(String methodName, int paramsCount) {
    for (int i = 0, n = methodNames.length; i < n; i++)
      if (methodNames[i].equals(methodName) && parameterTypes[i].length == paramsCount) return i;
    throw new IllegalArgumentException(
        "Unable to find non-private method: " + methodName + " with " + paramsCount + " params.");
  }

  public String[] getMethodNames() {
    return methodNames;
  }

  public Class<?>[][] getParameterTypes() {
    return parameterTypes;
  }

  public Class<?>[] getReturnTypes() {
    return returnTypes;
  }

  static public MethodAccess get(Class type) {
    boolean isInterface = type.isInterface();
    if (!isInterface && type.getSuperclass() == null && type != Object.class)
      throw new IllegalArgumentException("The type must not be an interface, a primitive type, or void.");

    ArrayList<Method> methods = new ArrayList<>();
    if (!isInterface) {
      Class<?> nextClass = type;
      while (nextClass != Object.class) {
        addDeclaredMethodsToList(nextClass, methods);
        nextClass = nextClass.getSuperclass();
      }
    } else
      recursiveAddInterfaceMethodsToList(type, methods);

    int n = methods.size();
    String[] methodNames = new String[n];
    Class<?>[][] parameterTypes = new Class[n][];
    Class<?>[] returnTypes = new Class[n];
    for (int i = 0; i < n; i++) {
      Method method = methods.get(i);
      methodNames[i] = method.getName();
      parameterTypes[i] = method.getParameterTypes();
      returnTypes[i] = method.getReturnType();
    }

    String className = type.getName();
    String accessClassName = className + "MethodAccess";
    if (accessClassName.startsWith("java.")) accessClassName = "reflectasm." + accessClassName;

    Class<?> accessClass;
    AccessClassLoader loader = AccessClassLoader.get(type);
    synchronized (loader) {
      accessClass = loader.loadAccessClass(accessClassName);
      if (accessClass == null) {
        String accessClassNameInternal = accessClassName.replace('.', '/');
        String classNameInternal = className.replace('.', '/');

        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        MethodVisitor mv;
        cw.visit(V1_1, ACC_PUBLIC + ACC_SUPER, accessClassNameInternal, null, Path.val+"/MethodAccess",
            null);
        {
          mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
          mv.visitCode();
          mv.visitVarInsn(ALOAD, 0);
          mv.visitMethodInsn(INVOKESPECIAL, Path.val+"/MethodAccess", "<init>", "()V", false);
          mv.visitInsn(RETURN);
          mv.visitMaxs(0, 0);
          mv.visitEnd();
        }
        {
          mv = cw.visitMethod(ACC_PUBLIC + ACC_VARARGS, "invoke",
              "(Ljava/lang/Object;I[Ljava/lang/Object;)Ljava/lang/Object;", null, null);
          mv.visitCode();

          if (!methods.isEmpty()) {
            mv.visitVarInsn(ALOAD, 1);
            mv.visitTypeInsn(CHECKCAST, classNameInternal);
            mv.visitVarInsn(ASTORE, 4);

            mv.visitVarInsn(ILOAD, 2);
            Label[] labels = new Label[n];
            for (int i = 0; i < n; i++)
              labels[i] = new Label();
            Label defaultLabel = new Label();
            mv.visitTableSwitchInsn(0, labels.length - 1, defaultLabel, labels);

            StringBuilder buffer = new StringBuilder(128);
            for (int i = 0; i < n; i++) {
              mv.visitLabel(labels[i]);
              if (i == 0)
                mv.visitFrame(Opcodes.F_APPEND, 1, new Object[] { classNameInternal }, 0, null);
              else
                mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
              mv.visitVarInsn(ALOAD, 4);

              buffer.setLength(0);
              buffer.append('(');

              Class<?>[] paramTypes = parameterTypes[i];
              Class<?> returnType = returnTypes[i];
              for (int paramIndex = 0; paramIndex < paramTypes.length; paramIndex++) {
                mv.visitVarInsn(ALOAD, 3);
                mv.visitIntInsn(BIPUSH, paramIndex);
                mv.visitInsn(AALOAD);
                Type paramType = Type.getType(paramTypes[paramIndex]);
                FieldAccess.switchType(mv, paramType);
                buffer.append(paramType.getDescriptor());
              }

              buffer.append(')');
              buffer.append(Type.getDescriptor(returnType));
              int invoke;
              if (isInterface)
                invoke = INVOKEINTERFACE;
              else if (Modifier.isStatic(methods.get(i).getModifiers()))
                invoke = INVOKESTATIC;
              else
                invoke = INVOKEVIRTUAL;
              mv.visitMethodInsn(invoke, classNameInternal, methodNames[i], buffer.toString(), false);

              switch (Type.getType(returnType).getSort()) {
                case Type.VOID:
                  mv.visitInsn(ACONST_NULL);
                  break;
                case Type.BOOLEAN:
                  mv.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false);
                  break;
                case Type.BYTE:
                  mv.visitMethodInsn(INVOKESTATIC, "java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;", false);
                  break;
                case Type.CHAR:
                  mv.visitMethodInsn(INVOKESTATIC, "java/lang/Character", "valueOf", "(C)Ljava/lang/Character;", false);
                  break;
                case Type.SHORT:
                  mv.visitMethodInsn(INVOKESTATIC, "java/lang/Short", "valueOf", "(S)Ljava/lang/Short;", false);
                  break;
                case Type.INT:
                  mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
                  break;
                case Type.FLOAT:
                  mv.visitMethodInsn(INVOKESTATIC, "java/lang/Float", "valueOf", "(F)Ljava/lang/Float;", false);
                  break;
                case Type.LONG:
                  mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false);
                  break;
                case Type.DOUBLE:
                  mv.visitMethodInsn(INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;", false);
                  break;
              }

              mv.visitInsn(ARETURN);
            }

            mv.visitLabel(defaultLabel);
            mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
          }
          mv.visitTypeInsn(NEW, "java/lang/IllegalArgumentException");
          mv.visitInsn(DUP);
          mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
          mv.visitInsn(DUP);
          mv.visitLdcInsn("Method not found: ");
          visit(mv);
          mv.visitMaxs(0, 0);
          mv.visitEnd();
        }
        cw.visitEnd();
        byte[] data = cw.toByteArray();
        accessClass = loader.defineAccessClass(accessClassName, data);
      }
    }
    try {
      MethodAccess access = (MethodAccess) accessClass.newInstance();
      access.methodNames = methodNames;
      access.parameterTypes = parameterTypes;
      access.returnTypes = returnTypes;
      return access;
    } catch (Throwable t) {
      throw new RuntimeException("Error constructing method access class: " + accessClassName, t);
    }
  }

  static void visit(MethodVisitor mv) {
    mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "(Ljava/lang/String;)V", false);
    mv.visitVarInsn(ILOAD, 2);
    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(I)Ljava/lang/StringBuilder;", false);
    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
    mv.visitMethodInsn(INVOKESPECIAL, "java/lang/IllegalArgumentException", "<init>", "(Ljava/lang/String;)V", false);
    mv.visitInsn(ATHROW);
  }

  static private void addDeclaredMethodsToList(Class<?> type, ArrayList<Method> methods) {
    Method[] declaredMethods = type.getDeclaredMethods();
    for (Method method : declaredMethods) {
      int modifiers = method.getModifiers();
      if (Modifier.isPrivate(modifiers)) continue;
      methods.add(method);
    }
  }

  static private void recursiveAddInterfaceMethodsToList(Class<?> interfaceType, ArrayList<Method> methods) {
    addDeclaredMethodsToList(interfaceType, methods);
    for (Class<?> nextInterface : interfaceType.getInterfaces())
      recursiveAddInterfaceMethodsToList(nextInterface, methods);
  }
}
